
#include "PortListener.h"
#include <QtDebug>

PortListener::PortListener(QObject *parent, Console *term) : QThread(parent)
{
    pload_verbose = 0;
    pload_delay = 0;
    resetType = RESET_BY_DTR;
    textEditor = term;
    connect(term, SIGNAL(sendSerial(QByteArray)), this, SLOT(send(QByteArray)));

#ifdef EVENT_DRIVEN
    this->port = new QextSerialPort(QextSerialPort::EventDriven);
    connect(this, SIGNAL(readyRead(QByteArray)), this, SLOT(onReadyRead(QByteArray)));
#else
    this->port = new QextSerialPort(QextSerialPort::Polling);
    connect(this, SIGNAL(readyRead(QByteArray)), this, SLOT(onReadyRead(QByteArray)));
#endif
    port->setPortName(""); // empty for now
}

void PortListener::init(const QString & portName, BaudRateType baud)
{
    port->setPortName(portName);
    port->setBaudRate(baud);
    port->setFlowControl(FLOW_OFF);
    port->setParity(PAR_NONE);
    port->setDataBits(DATA_8);
    port->setStopBits(STOP_1);
    port->setTimeout(10);
}

QString PortListener::getPortName()
{
    return port->portName();
}

BaudRateType PortListener::getBaudRate()
{
    return port->baudRate();
}

QextSerialPort *PortListener::getPort()
{
    return port;
}

void PortListener::setDtr(bool enable)
{
    this->port->setDtr(enable);
}

bool PortListener::open()
{
    if(!textEditor) // no text editor, no open
        return false;

    if(port == NULL)
        return false;

    bool rc = port->open(QIODevice::ReadWrite);
    if(rc) {
        running = true;
        start();
    }
    return rc;
}

void PortListener::close()
{
    if(port == NULL)
        return;

    running = false;
    flushPort();

    if(port->isOpen())
        port->close();
}

void PortListener::send(QByteArray data)
{
    port->write(data.constData(),1);
}

void PortListener::setTerminalWindow(Console *editor)
{
    textEditor = editor;
}

/*
 * Ok, this works for polling for most cases but fails a stress test
 * with gobs of data coming in. We really need event driven, and
 * now that Qt has QSerialPort, we can test with that. Maybe not today :)
 */
void PortListener::onReadyRead(QByteArray arr)
{
    int size = 0;

    /* Sometimes the runner thread gets killed,
     * but somehow onReadyRead is still queued.
     * If not running, return to not hog CPU.
     */
    if(!running) {
        return;
    }

    size = arr.count(); // buffer size

#if 0
    unsigned char *bytes = (unsigned char*)arr.data();
    qDebug() << "Queued count" << size;
    for(int n = 0; n < size; n++) {
        printf("%02x ", bytes[n]);
    }
    fflush(stdout);
#endif

    if(loadReceiveEnabled) {
        char *bytes = arr.data();
        for(int n = 0; n < size; n++) {
            rxqMutex.lock();
            rxqueue.append(bytes[n]);
            rxqMutex.unlock();
        }
        return;
    }

#if 1
    if(textEditor->getHexMode()) {
        for(int n = 0; n < size; n++)
            textEditor->dumphex((int)arr[n]);
        return;
    }
    else {
        for(int n = 0; n < size; n++)
            textEditor->update((char)arr[n]);
        arr.clear();
    }
#else
    if(!arr.contains("\b")) {
        textEditor->setPlainText(textEditor->toPlainText() + arr);
    }
    else {
        // if backspaces, do it the slow way.
        while(size-- > 0) {
            QTextCursor cur = textEditor->textCursor();
            cur.movePosition(QTextCursor::End,QTextCursor::MoveAnchor);

            QString text = textEditor->toPlainText();
            int tlen = text.length();
            if(arr.at(0) == '\b')
                textEditor->setPlainText(text.mid(0,tlen-1));
            else
                cur.insertText(QString(arr.at(0)));
            arr.remove(0,1);
            textEditor->setTextCursor(cur);
        }
    }
    textEditor->moveCursor(QTextCursor::End);
#endif
    textEditor->repaint(); // yay, all done! Keep this so screen updates.
}

void PortListener::run()
{
    while(running) {
        QByteArray bytes = port->readAll();
        int size = bytes.size();
        if(size > 0) {
            emit readyRead(bytes);
        }
        msleep(50);
        QApplication::processEvents();
    }
}

void PortListener::setStatusDialog(StatusDialog *dialog)
{
    statusDialog = dialog;
}

int PortListener::isDevice(QString portName)
{
    if(!statusDialog) {
        qDebug() << "Silly programmer. StatusDialog not initialized";
        return 0;
    }
    statusDialog->addMessage("Propeller");
    int rc = findprop(portName.toLatin1());
    if(!rc)  statusDialog->addMessage("not");
    statusDialog->addMessage("found on "+portName+"\n");
    return rc;

}

/**
 * receive a buffer with a timeout
 * @param buff - char pointer to buffer
 * @param n - number of bytes in buffer to read
 * @param timeout - timeout in milliseconds
 * @returns number of bytes read or SERIAL_TIMEOUT
 */
int PortListener::rx_timeout(char* buff, int n, int timeout)
{
    int size = 0;
    while(rxqueue.count() < 1) {
        msleep(1);
        QApplication::processEvents();
        if(timeout-- < 0)
            break;
    }
    while(timeout > 0 && rxqueue.count() > 0) {
        if(size >= n)
            break;
        rxqMutex.lock();
        buff[size] = rxqueue.at(0);
        rxqueue.remove(0,1);
        rxqMutex.unlock();
        size++;
    }
    return size == 0 ? SERIAL_TIMEOUT : size;
}

/**
 * transmit a buffer
 * @param buff - char pointer to buffer
 * @param n - number of bytes in buffer to send
 * @returns zero on failure
 */
int PortListener::tx(char* buff, int n)
{
    int size = port->write((const char*)buff, n);
    if(size != n) {
        qDebug() << "PortListener:tx failed. size" << size << "requested" << n;
    }
    return (size == n) ? 1 : 0;
}

/**
 * hwreset ... resets Propeller hardware using DTR or RTS
 * @param sparm - pointer to DCB serial control struct
 * @returns void
 */
void PortListener::hwreset(void)
{
    this->flushPort();
    this->port->flush();
    if(this->resetType == RESET_BY_DTR) {
        port->setDtr(true);
        msleep(10);
        port->setDtr(false);
    }
    else {
        port->setRts(true);
        msleep(10);
        port->setRts(false);
    }
    msleep(90);
    this->flushPort();
}

int PortListener::iterate(void)
{
    int bit = LFSR & 1;
    LFSR = (char)((LFSR << 1) | (((LFSR >> 7) ^ (LFSR >> 5) ^ (LFSR >> 4) ^ (LFSR >> 1)) & 1));
    return bit;
}

/**
 * getBit ... get bit from serial stream
 * @param hSerial - file handle to serial port
 * @param status - pointer to transaction status 0 on error
 * @returns bit state 1 or 0
 */
int PortListener::getBit(int* status, int timeout)
{
    char mybuf[2];
    int rc = rx_timeout(mybuf, 1, timeout);
    if(status)
        *status = rc <= 0 ? 0 : 1;
    return *mybuf & 1;
}

/**
 * getAck ... get ack from serial stream
 * @param hSerial - file handle to serial port
 * @param status - pointer to transaction status 0 on error
 * @returns bit state 1 or 0
 */
int PortListener::getAck(int* status, int timeout)
{
    char mybuf[2];
    int rc = rx_timeout(mybuf, 1, timeout);
    if(status)
        *status = rc <= 0 ? 0 : 1;
    return *mybuf & 1;
}

/**
 * makelong ... make an encoded long word to string
 * @param data - value to send
 * @param buff - char buffer
 * @returns nada
 */
void PortListener::makelong(int data, char* buff)
{
    int n = 0;
    //printf("0x%08x: ", data);
    for( ; n < 10; n++) {
        buff[n] = (char)(0x92 | (data & 1) | ((data & 2) << 2) | ((data & 4) << 4));
        data >>= 3;
    }
    buff[n] = (0xf2 | (data & 1) | ((data & 2) << 2));
    //for(n = 0; n < 11; n++) printf(" 0x%02x",(unsigned char)buff[n]);
    //printf("\n");
    //fflush(stdout);
}

/**
 * sendlong ... transmit an encoded long word to propeller
 * @param hSerial - file handle to serial port
 * @param data - value to send
 * @returns number of bytes sent
 */
int PortListener::sendlong(int data)
{
    char mybuf[12];
    makelong(data, mybuf);
    return tx(mybuf, 11);
}

/**
 * hwfind ... find propeller using sync-up sequence.
 * @param hSerial - file handle to serial port
 * @returns zero on failure
 */
int PortListener::hwfind(int retry)
{
    int  n, ii, jj, rc, to;
    char mybuf[300];

    /* hwfind is recursive if we get a failure on the first try.
     * retry is set by caller and should never be more than one.
     */
    if(retry < 0)
        return 0;

    loadReceiveEnabled = true;

    /* Do not pause after reset.
     * Propeller can give up if it does not see a response in 100ms of reset.
     */
    mybuf[0] = 0xF9;
    LFSR = 'P';  // P is for Propeller :)

    /* send the calibration pulse
     */
    if(tx(mybuf, 1) == 0)
        return 0;   // tx should never return 0, return error if it does.

    //if(rx_timeout(mybuf,10,10) > -1) { qDebug("Junk chars ..."); }

    /* Send the magic propeller LFSR byte stream.
     */
    for(n = 0; n < 250; n++)
        mybuf[n] = iterate() | 0xfe;
    if(tx(mybuf, 250) == 0)
        return 0;   // tx should never return 0, return error if it does.

    n = 0;
    while((jj = rx_timeout(mybuf,10,10)) > -1) {
        n += jj;
    }
    if(n != 0) qDebug("Ignored %d bytes. \n", n);

    /* Send 258 0xF9 for LFSR and Version ID
     * These bytes clock the LSFR bits and ID from propeller back to us.
     */
    for(n = 0; n < 258; n++)
        mybuf[n] = 0xF9;
    if(tx(mybuf, 258) == 0)
        return 0;   // tx should never return 0, return error if it does.

    /*
     * Wait at least 100ms for the first response. Allow some margin.
     * Some chips may respond < 50ms, but there's no guarantee all will.
     * If we don't get it, we can assume the propeller is not there.
     */
    ii = getBit(&rc, 110);
    if(rc == 0) {
        if (pload_verbose)
            qDebug("Timeout waiting for first response bit. Propeller not found.");
        return 0;
    }

    // wait for response so we know we have a Propeller
    for(n = 1; n < 250; n++) {

        jj = iterate();
        //if (pload_verbose) { printf("%d:%d %3d ", ii, jj, n); fflush(stdout); }

        if(ii != jj) {
            /* if we get this far, we probably have a propeller chip
             * but the serial port is in a funny state. just retry.
             */
            qDebug("Lost HW contact. %d %x ... retry.", n, *mybuf & 0xff);
            for(n = 0; (n < 300) && (rx_timeout(mybuf,1,10) > -1); n++);
            hwreset();
            return hwfind(--retry);
        }
        to = 0;
        do {
            ii = getBit(&rc, 100);
        } while(rc == 0 && to++ < 100);
        //printf(" %d\n", rc);
        if(to > 100) {
            qDebug("Timeout waiting for bit-stream response.");
            return 0;
        }
    }

    rc = 0;
    for(n = 0; n < 8; n++) {
        rc >>= 1;
        rc += getBit(0, 100) ? 0x80 : 0;
    }

    loadReceiveEnabled = false;
    if (pload_verbose) qDebug("Propeller Version ... %d", rc);
    return rc;
}

void PortListener::flushPort()
{
    int size = 0;
    do {
        //msleep(5);
        QApplication::processEvents();
        size = port->bytesAvailable();
        //if (pload_verbose) qDebug("Flushing port %d", size);
        port->readAll();
    } while(size > 0);
}

/**
 * Upload file image to propeller on port.
 * A successful call to findprop must be completed before calling this function.
 * @param file - filename for status message
 * @param dlbuf - pointer to file buffer
 * @param count - number of bytes in image
 * @param type - type of upload
 * @returns non-zero on error
 */
int PortListener::upload(char *dlbuf, int count, int type)
{
    int  n  = 0;
    int  rc = 0;
    int  wv = 100;
    int  ack = 0;
    int  remaining = 0;
    uint32_t data = 0;
    char buf[1];
    int  longcount = count/4;

    if(!statusDialog) {
        qDebug() << "Silly programmer. StatusDialog not initialized";
        return 0;
    }
#if !defined(Q_OS_MAC)
    // threading required for statusDialog animation is too much for Mac.
    statusDialog->init("Loading Program", "");
    statusDialog->addMessage("Loading Propeller on " + port->portName());
#endif
    loadReceiveEnabled = true;

    // send type
    if(sendlong(type) == 0) {
        printf("Sendlong type failed\n");
        return 1;
    }

    // send count
    if(sendlong(longcount) == 0) {
        printf("Sendlong count failed\n");
        return 1;
    }

    if((type == 0) || (type >= DOWNLOAD_SHUTDOWN)) {
        printf("Shutdown type %d sent\n", type);
        return 1;
    }

    remaining = count;
    for(n = 0; n < count; n+=4) {
        if(n % 1024 == 0) {
            if(pload_verbose) {
                printf("%d bytes remaining\n", remaining);
                fflush(stdout);
            }
            remaining -= 1024;
        }

        //data = (int) dlbuf[n] | (dlbuf[n+1] << 8) | (dlbuf[n+2] << 16) | (dlbuf[n+3] << 24);

        data = 0;
        unsigned char num;
        for(int j = 0; j < 4; j++) {
            num = (unsigned char)dlbuf[n+j];
            data |= num << j*8;
        }

        if(sendlong(data) == 0) {
            printf("Sendlong error");
            return 1;
        }
    }
    if(pload_verbose) {
        printf("%d bytes sent\n", count);
        fflush(stdout);
    }

    msleep(50);                 // give propeller time to calculate checksum match 32K/12M sec = 32ms
    buf[0] = 0xF9;              // need to send this to make Propeller send the ack

    /*
     * Check for a RAM program complete signal
     */
    statusDialog->setMessage("Verifying Propeller RAM");
    if(pload_verbose) printf("Verifying RAM ... ");

    for(n = 0; n < wv; n++) {
        if(tx(buf, 1) == 0) return 1;
        rc = getAck(&ack, 50);
        if(ack) break;
    }

    loadReceiveEnabled = false;

    /*
     * Check for a Timeout or Checksum Error
     */
    if(n >= wv) {
        statusDialog->setMessage("Programming Timeout Error.");
        statusDialog->stop(8);
        if(pload_verbose) printf("Timeout Error!\n");
        return 1;
    }

    if(rc == 0) {
        if(pload_verbose) {
            printf("Verify RAM (%d) OK\n", rc);
            fflush(stdout);
        }
    }
    else {
        statusDialog->setMessage("Programming Checksum Error.");
        statusDialog->stop(8);
        if(pload_verbose) {
            printf("(%d) Checksum Error!\n", rc);
            fflush(stdout);
        }
        return 1;
    }

    /* If type is DOWNLOAD_EEPROM or DOWNLOAD_RUN_EEPROM
     * Check for a complete signal, then check for verify signal
     * Otherwise the board will shutdown in an undesirable way.
     */
    if(type & DOWNLOAD_EEPROM) {
        wv = 500;

        loadReceiveEnabled = true;
        /* send this to make Propeller send the ack
         */
        buf[0] = 0xF9;

        statusDialog->setMessage("Programming EEPROM");
        if(pload_verbose) {
            printf("Programming EEPROM ... ");
            fflush(stdout);
        }

        /* Check for EEPROM program finished
         */
        for(n = 0; n < wv; n++) {
            msleep(20);
            if(tx(buf, 1) == 0) return 1;
            rc = getAck(&ack, 20);
            if(ack) {
                if(rc != 0) {
                    statusDialog->setMessage("EEPROM Programming Error.");
                    statusDialog->stop(8);
                    if(pload_verbose) {
                        printf("Failed\n");
                        fflush(stdout);
                    }
                    return 1;
                }
                break;
            }
        }

        if(n >= wv) {
            statusDialog->setMessage("EEPROM Programming Timeout.");
            statusDialog->stop(8);
            if(pload_verbose) {
                printf("EEPROM programming timeout\n");
                fflush(stdout);
            }
            loadReceiveEnabled = false;
            return 1;
        }

        if(rc == 0) {
            if(pload_verbose) {
                printf("Program EEPROM (%d) OK\n", rc);
                fflush(stdout);
            }
        }
        else {
            statusDialog->setMessage("EEPROM Programming Error.");
            statusDialog->stop(8);
            if(pload_verbose) {
                printf("(%d) Failed\n", rc);
                fflush(stdout);
            }
            loadReceiveEnabled = false;
            return 1;
        }

        if(pload_verbose) {
            printf("Verifying EEPROM Checksum ... ");
            fflush(stdout);
        }
        /* Check for EEPROM program verify
         */
        for(n = 0; n < wv; n++) {
            msleep(20);
            if(tx(buf, 1) == 0) return 1;
            rc = getAck(&ack, 20);
            if(ack) {
                if(rc != 0) {
                    statusDialog->setMessage("EEPROM Verify Program Error.");
                    statusDialog->stop(8);
                    if(pload_verbose) {
                        printf("Failed\n");
                        fflush(stdout);
                    }
                    return 1;
                }
                break;
            }
        }

        loadReceiveEnabled = false;
        if(n >= wv) {
            statusDialog->setMessage("EEPROM Verify Timeout Error.");
            statusDialog->stop(8);
            if(pload_verbose)
                printf("EEPROM verify timeout\n");
            return 1;
        }

        if(rc == 0) {
            if(pload_verbose) {
                printf("(%d) OK\n", rc);
                fflush(stdout);
            }
        }
        else {
            statusDialog->setMessage("EEPROM Verify Program Error.");
            statusDialog->stop(8);
            if(pload_verbose) {
                printf("(%d) Failed\n", rc);
                fflush(stdout);
            }
            return 1;
        }
    }

    statusDialog->setMessage("Propeller Programming OK!");
    statusDialog->stop(4);
    return 0;
}

/**
 * find a propeller on port
 * @param hSerial - file handle to serial port
 * @param sparm - pointer to DCB serial control struct
 * @param port - pointer to com port name
 * @returns non-zero on error
 */
int PortListener::findprop(const char* name)
{
    int version = 0;

    if (pload_verbose)
        qDebug("\nChecking for Propeller on port %s", name);

    if(!port)
        return -1;

    if(port->isOpen())
        close();

    init(name, BAUD115200);
    bool rc = open();
    if(!rc) {
        close();
        qDebug() << "Can't open port" << name;
        return 0;
    }

    flushPort();

    /*
     * TODO Queue is broken. Fix it later.
     * For the time being the current queue buffer is big enough to hold
     * all expected receive data + garbage from quickstart boards.
     */

    hwreset();
    version = hwfind(1); // retry once

    if (pload_verbose) {
        if(version) {
            qDebug() << "Propeller Version" << version << "on" << name;
        } else {
            qDebug() << "Propeller not found on" << name;
        }
    }
    close();

    return version != 0 ? 1 : 0;
}

/**
 * pupload is a wrapper for findprop + upload
 * This function should be used so that we don't get timeouts between
 * findprop and upload that can be caused by slow PC/Linux/MAC hardware.
 * @param file - filename for status message
 * @param dlbuf - pointer to file buffer
 * @param count - number of bytes in image
 * @param type - type of upload
 * @returns non-zero on error
 */
int PortListener::pupload(QString file, char *dlbuf, int count, int type)
{
    int rc = -1;
    int version = 0;

    if (file.length() > 0) {
        printf("Loading '");
        printf("%s",file.toLatin1().data());
        printf("'");
    }
    else {
        printf("Loading");
    }
    if(type == DOWNLOAD_RUN_BINARY)
        printf(" to hub memory\n");
    else
        printf(" to EEPROM via hub memory\n");

    if(0) {
        int sum = 0;
        int n = 0;
        for(; n < count; n++) {
            if(n && n % 100 == 0)
                printf("Sum @%d %d\n", n, sum);
            sum += (char) dlbuf[n];
        }
        printf("Sum @count %d %d\n", n, sum);
        fflush(stdout);
    }

    hwreset();
    version = hwfind(1); // retry once
    if(version) {
        rc = upload(dlbuf, count, type);
    }
    return rc;
}

/**
 * find and load propeller with file - assumes port is already open
 * @returns non-zero on error.
 */
int PortListener::load(QString fileName, PortListener::DOWNLOAD type)
{
    int rc = 1;
    QByteArray data;

    if(!fileName.length())
        return -1;

    if(!port)
        return -1;

    if(!port->isOpen())
        open();

    if(!port->isOpen())
        return -1;

    QFile file(fileName);
    if (file.open(QFile::ReadOnly))
    {
        QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
        data = file.readAll();
        file.close();
#if 0
        for(int n = 0; n < data.count(); n++) {
            if(n % 16 == 0)
                printf("\n %02x", (unsigned char) data[n]);
            else
                printf(" %02x", (unsigned char) data[n]);
        }
        printf("\n");
        fflush(stdout);
#endif
        // if found and file, upload file
        rc = pupload(fileName, data.data(), data.length(), type);
        QApplication::restoreOverrideCursor();
    }

    return rc;
}

